Skip to content

feat: add My Account API support for managing MFA authentication method#835

Open
utkrishtsahu wants to merge 6 commits into
mainfrom
feature/my-account-api
Open

feat: add My Account API support for managing MFA authentication method#835
utkrishtsahu wants to merge 6 commits into
mainfrom
feature/my-account-api

Conversation

@utkrishtsahu
Copy link
Copy Markdown
Contributor

  • All new/changed/fixed functionality is covered by tests (or N/A)
  • I have added documentation for all new/changed functionality (or N/A)

📋 Changes

Adds support for the Auth0 My Account API, enabling end-users to self-manage their MFA authentication methods without requiring Management API tokens.

New public API:

  • Auth0.myAccount(accessToken:) — factory method returning a MyAccountApi instance
  • MyAccountApi class with the following methods:
  • getAuthenticationMethods() — list all enrolled authentication methods
  • getAuthenticationMethod(id:) — get a specific method by ID
  • deleteAuthenticationMethod(id:) — delete a method by ID
  • getFactors() — list available factors on the tenant
  • enrollPhone(phoneNumber:, type:) — enroll phone (SMS/voice)
  • enrollEmail(email:) — enroll email OTP
  • enrollTotp() — enroll TOTP (authenticator app)
  • enrollPush() — enroll push notifications
  • enrollRecoveryCode() — enroll recovery codes
  • verifyOtp(id:, authSession:, otp:) — confirm enrollment with OTP

New types added:

  • MyAccountApi — main API class
  • AuthenticationMethod — represents an enrolled MFA method
  • EnrollmentChallenge — returned by enrollment methods (contains id, authSession, and type-specific fields like totpSecret, totpUri, barcodeUri, recoveryCode)
  • Factor — represents an available factor (name, enabled)
  • PhoneType — enum (sms, voice)
  • MyAccountException — domain exception with statusCode, isNetworkError, isRetryable

Platform implementation:

  • Android: Auth0FlutterMyAccountMethodCallHandler + 10 individual request handlers via MyAccountAPIClient
  • iOS/macOS: MyAccountHandler + 10 individual method handlers via Auth0.swift MyAccount client
  • Method channel: auth0.com/auth0_flutter/my_account

Requires access tokens with audience https://{domain}/me/ and appropriate me scopes (read:me:authentication_methods, create:me:authentication_methods, delete:me:authentication_methods, read:me:factors).

📎 References

SDK-8730

🎯 Testing

Unit tests:

  • Android: 11 new test files covering all request handlers + method call handler routing (247 tests total, all passing)
  • iOS: 39 new tests covering all method handlers via spy/mock objects (250 tests total, all passing)

Manual testing:

  • Tested on iOS Simulator (iPhone 16, iOS 18.4) and Android emulator
  • Verified full enrollment flow: login with My Account audience → enroll phone (SMS) → verify OTP → list methods → delete method
  • Verified TOTP enrollment returns correct totpSecret and totpUri for authenticator app integration

Not tested:

  • Web platform (not supported )

private const val MY_ACCOUNT_DELETE_AUTH_METHOD_METHOD =
"myAccount#deleteAuthenticationMethod"

class DeleteAuthenticationMethodRequestHandler :
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is final class required here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, final class is not used here — it's a regular class implementing the MyAccountRequestHandler interface. Since it's implementing an interface, it can't be final. No change needed.

override fun onSuccess(
res: AuthenticationMethod
) {
result.success(null)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is flutter compatible with suspend functions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flutter's MethodChannel on Android uses callback-based MethodChannel.Result, not coroutines. The Auth0 Android SDK's Request.start(Callback) pattern is the correct approach for MethodChannel handlers. Suspend functions would require a coroutine scope which adds unnecessary complexity here. This is the same pattern used by all other handlers in the plugin (WebAuth, Auth, CredentialsManager).

result.success(res.map {
mapOf(
"name" to it.type,
"enabled" to true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Factor has type and usage. Dont we want to pass usage as well to dart?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, usage array should be returned

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Updated to pass "type" and "usage" directly from the native Factor object instead of mapping to "name"/"enabled".

result.success(res.map {
mapOf(
"name" to it.type,
"enabled" to true
Copy link
Copy Markdown
Contributor

@sanchitmehtagit sanchitmehtagit May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"enabled" : true should this be derived from response.?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Removed enabled entirely — the API doesn't return an enabled property. Replaced with "usage" which is the actual field from the SDK (List? like ["secondary"]).

override fun onSuccess(res: List<Factor>) {
result.success(res.map {
mapOf(
"name" to it.type,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets keep it as type only. Its not actually a name thats returned.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Changed from "name" to it.type to "type" to it.type.

result.success(res.map {
mapOf(
"name" to it.type,
"enabled" to true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly does this enabled represent. The API doesn't return an enabled property

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — removed it. Replaced with the actual usage array from the SDK. Updated the Dart Factor model accordingly (type + usage instead of name + enabled).

request: MethodCallRequest,
result: MethodChannel.Result
) {
client.getAuthenticationMethods()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API now supports a type parameter as input. Android PR is already up.
auth0/Auth0.Android#974
Both platforms will have this out by next week. Update accordingly

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. Will add the optional type filter parameter to getAuthenticationMethods() once Auth0.Android PR #974 is merged and the corresponding Swift SDK update is available. Will address in a follow-up commit/PR.

override fun onSuccess(
res: AuthenticationMethod
) {
result.success(null)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the success returning null here. Shouldn't the details from AuthenticationMethod type be returned here ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Now returning res.toMyAccountMethodMap() on success. Updated the Dart return type from Future to Future across the platform interface and public API. iOS handler also updated to return method.asDictionary().

val handler = myAccountRequestHandlers.find { it.method == call.method }
if (handler != null) {
val accessToken = request.data["accessToken"] as? String ?: ""
val client = MyAccountAPIClient(request.account, accessToken)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MyAccountAPI's support DPoP too. Ensure the client created can support DPoP also.
auth0/Auth0.Android#974
This PR adds the support for the same in Android

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. Will add DPoP support to the MyAccountAPIClient creation once Auth0.Android PR #974 is merged and the Swift SDK adds equivalent support. This will involve passing a useDPoP flag from Dart and configuring the client accordingly. Will address in a follow-up once both native SDKs have this merged.

put("_statusCode", exception.statusCode)
put("_errorFlags", mapOf(
"isNetworkError" to exception.isNetworkError,
))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MyAccountException returns more info which actually represents what went wrong like title, detail etc. These help the customer know what went wrong and also help us debug when an issue arises. Currently the above map captures only the status code, which in itself is not helpful. Add the other properties too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added title and detail to the error map on both Android and iOS. Also exposed them as first-class properties on the Dart MyAccountException class (exception.title, exception.detail).

return buildMap {
put("id", id)
put("type", type)
put("created_at", createdAt)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a usage property also

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added usage to the AuthenticationMethod serialization (inside the MfaAuthenticationMethod check alongside confirmed). Also added it to the Dart model.

is PushNotificationAuthenticationMethod -> {
put("name", method.name)
}
else -> {}
Copy link
Copy Markdown
Contributor

@pmathew92 pmathew92 May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passkey , Recovery code and Password are missing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an else branch that sets name to null for unhandled subtypes (Passkey, RecoveryCode, Password). These types don't have additional properties beyond the base id, type, createdAt that are already serialized for all AuthenticationMethod instances. Full passkey enrollment support is deferred to a future release due to WebAuthn/FIDO2 platform complexity.

put("name", method.name)
}
is PushNotificationAuthenticationMethod -> {
put("name", method.name)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The confirmed property is also missing which is important to know whether a authentication method is verfied or nor

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. confirmed was already being serialized on Android (inside the MfaAuthenticationMethod check at line 39). Also added it to the iOS AuthenticationMethod.asDictionary() and the Dart AuthenticationMethod model so it's accessible as method.confirmed.

put("recovery_code", challenge.recoveryCode)
}
is MfaEnrollmentChallenge -> {}
else -> {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oob enrollment challenge is missing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. Will add OOB (Out-of-Band) enrollment challenge support in a follow-up once the full OOB enrollment flow is confirmed across both native SDKs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants